﻿/*
ParallelPlatform Project - A web platform to distribute and share knowledge
Online Editor - An online tool to create and play animations
Copyright (C) 2011 Daniel Guerrero Martínez & Sergio Rodríguez Lumley

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package Classes{
	import flash.display.MovieClip;
	import flash.utils.getDefinitionByName;
	import flash.display.DisplayObject;
	import flash.geom.Point;
	
	public class Container extends Panel {
		
		public static const NORMAL:uint = 0;
		public static const INSTANT:uint = 1;
		public static const READER:uint = 2;
		
		/**
		  Translates a string with the type to the code of Container.type
		  @param str String with the type
		  @return uint with the correct code of the container
		*/
		public static function typeStringToCode(str:String):uint{
			var ret:uint = NORMAL;
			if(str.search("normal") >= 0) ret = NORMAL;
			else if(str.search("instant") >= 0) ret = INSTANT;
			else if(str.search("reader") >= 0) ret = READER;
			return ret;
		}
		
		private var ID:uint; // ID of the Stage
		private var type:uint;
		private var nextObjID:uint; // ID of the next object to create
		private var actions:ActionList;
		private var actionsIndex:Vector.<uint>;
		private var redoActions:ActionList;
		private var redoActionsIndex:Vector.<uint>;
		private var removedShapes:Vector.<ShapeObject>;
		private var removedShapesIndexes:Vector.<uint>;
		private var selectFunction:Function;
		
		// Reference to the backPanel that canvas would add to this container
		// when being edited
		private var backPanel:Panel;
		
		/**
		  Constructor
		*/
		public function Container(myID:uint = 0){
			ID 				= myID;
			type 			= NORMAL;
			nextObjID 		= 0;
			backPanel 		= null;
			actions 		= new ActionList();
			redoActions 	= new ActionList();
			selectFunction 	= null;
			removedShapes	= new Vector.<ShapeObject>();
			removedShapesIndexes = new Vector.<uint>();
			actionsIndex	= new Vector.<uint>();
			redoActionsIndex = new Vector.<uint>();
		}
		
		/**
		  Set the type of the Scene (See static values in Container)
		*/
		public function setType(typ:uint):void{
			type = typ;
		}
		
		/**
		  Gets the type of the Scene (See static values in Container)
		*/
		public function getType():uint{
			return type;
		}
		
		/**
		  Gets the number of shapes the container has
		  @return number of shapes
		*/
		public function getNumShapes():uint{
			if(backPanel != null){
				return numChildren -2;
			}
			return numChildren -1;
		}
		
		/**
		  Adds a ShapeObject to the specified coordinates
		  @param shapeToCreate String that specifies the class of the Shape
		  @param localX Number with the coordinate X where to create the shape, centered
		  @param localY Number with the coordinate Y where to create the shape, centered
		*/
		public function addShapeXY(shapeToCreate:String, localX:Number, localY:Number):ShapeObject{
			return this.createShape(null, shapeToCreate, localX, localY, selectFunction);
		}
		
		/**
		  Sets the specified shapeObject to the indicated depth, leaving always the original
		  panel below, and the BackPanel if there is one
		  @param obj ShapeObject to change the depth, it is assumed this container has that ShapeObject
		  @param depth uint with the new depth (absolute value)
		  @return int with the new depth
		*/
		public function setShapeDepth(obj:ShapeObject, depth:uint):int{
			var offset:int = 1;
			if(backPanel != null) ++offset;
			if(depth+offset >= numChildren) depth = numChildren-offset-1;
			
			this.setChildIndex(obj, depth+offset);
			
			return depth;//+offset;
		}
		
		/**
		  Gets the depth of the specified ShapeObject
		  @return uint with the depth of the ShapeObject
		*/
		public function getShapeDepth(obj:ShapeObject):uint{
			var offset:int = 1;
			if(backPanel != null) ++offset;
			
			return getChildIndex(obj) - offset;
		}
		
		/**
		  Gets the specified ShapeObject, shapes go from 0 to getNumShapes()
		  @param num uint with the position of the shapeObject
		  @return the specified ShapeObject
		*/
		public function getShapeAt(num:uint):ShapeObject{
			var offset:int = 1;
			if(backPanel != null) ++offset;
			
			return getChildAt(num+offset) as ShapeObject;
		}
		
		/**
		  Gets the ShapeObject with ID specified
		  @param id String with the FullID of the shapeObject
		  @return the specified ShapeObject
		*/
		public function getShape(id:String):ShapeObject{
			return getChildByName(id) as ShapeObject;
		}
		
		/**
		  Sets the selection function to all the shapes inside
		  @param func Function with the selection function
		*/
		public function setSelectionFunction(func:Function):void{
			selectFunction = func;
			var i:int = 1;
			if(backPanel != null) ++i;
			for(; i<numChildren; ++i){
				(this.getChildAt(i) as ShapeObject).setSelectionFunction(selectFunction);
			}
		}
		
		/**
		  Overwrites the current action list for the new specified
		  @param acts ActionList with the new actions
		*/
		public function setActions(acts:ActionList):void{
			actions = acts;
		}
		
		/**
		  Returns the action list of this container
		*/
		public function getActions():ActionList{
			return actions;
		}
		
		/**
		  Sets the action index (clustering actions to undo and redo)
		*/
		public function setActionIndex(actindx:Vector.<uint>):void{
			actionsIndex = actindx;
		}
		
		/**
		  Adds an action to the action list and empties the redo-action list
		  @param act Action to add in the list
		  @param watermark Boolean indicates if this is the first action of a series
		  	of actions, opening a new a cluster. This makes the system to undo sets of
			related actions instead of undoing them one by one. By default every action
			is a cluster itself, so by default value is true.
		*/
		public function addAction(act:Action, watermark:Boolean = true):void{
			if(watermark){
				actionsIndex.push(actions.size());
			}
			actions.push(act);
			if(redoActions.size() > 0){
				redoActionsIndex = new Vector.<uint>();
				redoActions = new ActionList();
			}
		}
		
		/**
		  Gets the number of redo clustered actions
		  @return uint with the number of redo clustered actions
		*/
		public function getNumRedoActions():uint{
			return redoActionsIndex.length;
		}
		
		/**
		  Overwrites the current extras so that containers will make a perfect
		  copy
		  @param extras Array with redoActions, removedShapes, removedShapesIndexes and type
		*/
		public function setExtras(extras:Array):void{
			redoActions 			= extras[0];
			removedShapes 			= extras[1];
			removedShapesIndexes 	= extras[2];
			type					= extras[3];
			actionsIndex			= extras[4];
			redoActionsIndex		= extras[5];
		}
		
		/**
		  Returns an array with all the extras that could be needed
		  to do a copy of this container
		  @return Array with redoActions, removedShapes, removedShapesIndexes and type
		*/
		public function getExtras():Array{
			var arr:Array = new Array();
			arr[0] = redoActions;
			arr[1] = removedShapes;
			arr[2] = removedShapesIndexes;
			arr[3] = type;
			arr[4] = actionsIndex;
			arr[5] = redoActionsIndex;
			return arr;
		}
		
		/**
		  Adds a backPanel to the container, it is differenced because it is not a shape
		*/
		public function addBackPanel(panel:Panel):void{
			backPanel = panel;
			this.addChildAt(backPanel, 1);
		}
		
		/**
		  Removes the added backPanel of the container, this is a safe
		  operation, the existance of a backPanel if checked before
		  attempting to remove it
		*/
		public function removeBackPanel():void{
			if(backPanel != null){
				this.removeChild(backPanel);
				backPanel = null;
			}
		}
		
		/**
		Sets the ID of the container
		@param i ID to set
		*/
		public function setID(i:uint):void{
			ID = i;
		}
		
		/**
		Gets the ID of the container
		@return int with the ID
		*/
		public function getID():uint{
			return ID;
		}
		
		/**
		  Removes the specified shape from the container,
		  it adds it to the removedShapes list
		  @param shap ShapeObject to remove, it must be contained in container
		*/
		public function removeShape(shap:ShapeObject):void{
			removedShapesIndexes.push(this.getChildIndex(shap));
			this.removeChild(shap);
			shap.clean();
			removedShapes.push(shap);
		}
		
		/**
		  Undoes the last cluster of actions if its possible, removes them from the
		  action list and stores them in the redo-action list
		*/
		public function undo():void{
			if(actionsIndex.length > 0){
				redoActionsIndex.push(redoActions.size());
				while(actions.size() > actionsIndex[actionsIndex.length-1]){
					undoOneAction();
				}
				actionsIndex.pop();
			}
		}
		
		/**
		  Redoes the last set of actions if its possible and stores them in
		  the action list again
		*/
		public function redo():void{
			if(redoActionsIndex.length > 0){
				actionsIndex.push(actions.size());
				while(redoActions.size() > redoActionsIndex[redoActionsIndex.length-1]){
					redoOneAction();
				}
				redoActionsIndex.pop();
			}
		}
		
		/**
		  Duplicates this container returning a new one, this duplication is NOT
		  a clone, this means it is not exactly the same, as the new container
		  will not have any defined actions, backPanel or events
		*/
		public function duplicate():Container{
			var dup:Container = new Container();
			var tmp:ShapeObject;
			dup.setID(this.getID());
			dup.setType(this.getType());
			dup.nextObjID = 0;
			
			var i:uint = 1;
			if(backPanel != null){
				this.setChildIndex(backPanel, 1);
				++i;
			}
			for(; i< numChildren; ++i){
				//tmp = this.getChildAt(i);
				//if(getQualifiedClassName(tmp) != "Classes::Panel")
				//	dup.addChild((tmp as ShapeObject).duplicate());
				tmp = this.getChildAt(i) as ShapeObject;
				if(! tmp.isRemoved())
					dup.addChild(tmp.duplicate());
			}
			
			return dup;
		}
		
		/**
		  Invalidates all the actions that where made before thisone and meet
		  the specifications
		  @param acttype Uint specifying the type of action to invalidate
		  @param shapid String with full ID of the Shape of which we want to invalidate the actions
		  @param thisindex Uint with the index of this action, to invalidate just the ones before,
		     if value is zero, all will be invalidated (default value 0)
		  @return Array with the indexes of invalidated actions, null if non has been invalidated
		*/
		public function invalidateActions(acttype:uint, shapid:String, thisindex:uint=0):Array{
			var array:Array = new Array();
			var i:uint = 0;
			var action:Action;
			
			array.push(acttype);
			if(thisindex == 0) thisindex = actions.size() +1;
			thisindex = Math.min(actions.size(), thisindex);
			for(i=0; i<thisindex; ++i){
				action = actions.get(i);
				if(action.getType() == acttype &&
					action.getShapeID() == shapid){
					array.push(i);
					action.invalidate();
				}
			}
			
			if(array.length <= 1) array = null;
			return array;
		}
		
		/**
		  Applies all the actions in the ActionsList independently of which ones have been commited
		*/
		public function applyActions():void{
			for(var i:uint = 0; i< actions.size(); ++i){
				applyAction(actions.get(i));
			}
		}
		
		/**
		  Cleans the whole container and the shapes inside it, clean removes all
		  event listeners and leaves the container as light as possible
		*/
		public function cleanAll():void{
			var i:uint = 1;
			if(backPanel != null)
				++i;

			for(; i<numChildren; ++i)
				ShapeObject(this.getChildAt(i)).clean();
				
			selectFunction = null;
		}
		
		/**
		  Clears all the contained shapes an all the eventListeners asociated
		  and removes all children
		*/
		public function removeAll():void{
			this.removeBackPanel();

			while(this.numChildren > 1){
				ShapeObject(this.getChildAt(numChildren-1)).clean();
				this.removeChildAt(numChildren-1);
			}
			selectFunction = null;
		}
		
		public function printActions():String{
			var str:String = "";
			var i:uint = 0;
			var j:uint = 0;
			for(i = 0; i<actions.size(); ++i){
				for(j=0; j<actionsIndex.length; ++j){
					if(actionsIndex[j] == i) str += "\n"+String(i)+" ";
				}
				str += "["+actions.get(i).printString() +"] ";
			}
			return str;
		}
		
		// ************ PRIVATE FUNCTIONS ************* //
		
		/**
		  Applys the specified action
		  @param act Action to apply
		*/
		private function applyAction(act:Action):void{
			var tmp:ShapeObject;
			// Check if object exists still
			if(act.getType() != Action.INSERT && 
							act.getType() < Action.INVALID){
				tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
				if(tmp == null){
					//trace(act.getType() +" is being invalidated!");
					act.invalidate();
				}
			}
			switch(act.getType()){
				case Action.INSERT:
					//tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					//trace(tmp);
					//if(tmp == null){
					this.createShape(act.getShapeID(), act.getString(0), act.getNumber(0), act.getNumber(1), selectFunction);
					//}else{
					//	tmp.unremove();
					//}
					//++nextObjID;
					break;
				case Action.REMOVE:
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					if(tmp.getStageID() == ID){ // It was created in this stage
						this.removeShape(tmp);
					}else{
						tmp.remove();
					}
					break;
				case Action.MOVE:
					(this.getChildByName(act.getShapeID()) as ShapeObject).moveTo(act.getNumber(2), act.getNumber(3));
					break;
				case Action.SCALE:
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					tmp.scaleX = act.getNumber(2);
					tmp.scaleY = act.getNumber(3);
					break;
				case Action.ROTATE:
					(this.getChildByName(act.getShapeID()) as ShapeObject).rotation = act.getNumber(1);
					break;
				case Action.CHANGE_DEPTH:
					this.setShapeDepth(this.getChildByName(act.getShapeID()) as ShapeObject, act.getNumber(1));
					break;
				case Action.CHANGE_TEXT:
					(this.getChildByName(act.getShapeID()) as ShapeObject).setText(act.getString(1));
					break;
				case Action.INVALIDATE_REMOVE:
					(this.getChildByName(act.getShapeID()) as ShapeObject).unremove();
					break;
				case Action.BLUR:
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					tmp.setBlur(act.getNumber(2), act.getNumber(3));
					break;
				case Action.PAINT:
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					tmp.setColors(this.getPaintColors(act));
					tmp.setLines(this.getPaintLines(act));
					tmp.repaint();
					break;
					
				case Action.INVALIDATE_PREV_ACTIONS:
					//this.invalidateActions(uint(act.getNumber(0)), act.getShapeID(), actions.getIndex(act));
					this.redoActionInvalidation(act);
					//trace("Container: applyActions() -> Do not regenerate here the INVALIDATE_PREV_ACTIONS, but apply it's stored data");
					break;
				default:
					//trace("Container: applyAction() -> Found "+ Action.translateToString(act.getType()-1000)+ " from "+act.getShapeID() +" that has been invalidated");
					break;
			}
		}
		
		/**
		  Undoes the specified action by doing exaclty the oposite
		  @param act Action to undo
		*/
		private function undoAction(act:Action):void{
			var tmp:ShapeObject;
			
			switch(act.getType()){
				case Action.INSERT: // Insert? Then we remove it!
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					if(tmp.getStageID() == ID){ // It was created in this stage
						this.removeChild(tmp);
					}else{
						tmp.remove();
					}
					break;
				case Action.REMOVE: // Remove? Then we we unremove if it wasn't created here, or we restore it
					// We look if the Shape exists in this stage, if it was created
					// in this stage, then it wont exist
					if(uint(ShapeObject.SceneOf(act.getShapeID())) == this.ID){
						restoreRemovedShape(act.getShapeID());
					}else{ // We just unremove it
						(this.getChildByName(act.getShapeID()) as ShapeObject).unremove();
					}
					break;
				case Action.MOVE:
					(this.getChildByName(act.getShapeID()) as ShapeObject).moveTo(act.getNumber(0), act.getNumber(1));
					break;
				case Action.SCALE:
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					tmp.scaleX = act.getNumber(0);
					tmp.scaleY = act.getNumber(1);
					break;
				case Action.ROTATE:
					(this.getChildByName(act.getShapeID()) as ShapeObject).rotation = act.getNumber(0);
					break;
				case Action.CHANGE_DEPTH:
					this.setShapeDepth(this.getChildByName(act.getShapeID()) as ShapeObject, act.getNumber(0));
					break;
				case Action.CHANGE_TEXT:
					(this.getChildByName(act.getShapeID()) as ShapeObject).setText(act.getString(0));
					break;
				case Action.INVALIDATE_REMOVE:
					// A good thing would be to check if it was created in this same
					// stage, but for now it is not likely an invalidate_remove action
					// would get here, because when the shape was removed at first then
					// it was checked if it was created in this same stage, and completely
					// removed
					(this.getChildByName(act.getShapeID()) as ShapeObject).remove();
					break;
					
				case Action.BLUR:
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					tmp.setBlur(act.getNumber(0), act.getNumber(1));
					break;
				
				case Action.PAINT:
					tmp = this.getChildByName(act.getShapeID()) as ShapeObject;
					tmp.clearPaint();
					break;
					
				case Action.INVALIDATE_PREV_ACTIONS:
					undoActionInvalidation(act);
					break;
					
				default:
					//trace("Container: applyAction() -> Found "+ Action.translateToString(act.getType()-1000)+ " from "+act.getShapeID() +" that has been invalidated");
					break;
			}
		}
		
		/**
		  Creates a ShapeObject and adds it to the container with the specified coordinates and ID
		  @param fullID String with the full ID of the shape to create
		  @param shapeToCreate String that specifies the class of the Shape
		  @param localX Number with the coordinate X where to create the shape, centered
		  @param localY Number with the coordinate Y where to create the shape, centered
		  @param shapeSelect Function to trigger each time the shape is selected, null to not trigger anything (default)
		*/
		private function createShape(fullID:String, shapeToCreate:String, localX:Number, localY:Number, shapeSelect:Function=null):ShapeObject{
			var classReference:Class = getDefinitionByName(shapeToCreate) as Class;
			var tmp:MovieClip = new classReference();
			tmp.setSelectionFunction(shapeSelect);
			if(fullID == null){
				tmp.setID(nextObjID);
				++nextObjID;
				tmp.setStageID(ID);
			}else{
				tmp.setFullID(fullID);
				nextObjID = Math.max(nextObjID, tmp.getID()+1);
			}
			tmp.name = tmp.getFullID();
			addChild(tmp);
			tmp.x = localX;//-tmp.width/2;
			tmp.y = localY;//-tmp.height/2;
			return ShapeObject(tmp);
		}
		
		/**
		  Pops the removed shape object from the removedShapes list
		  and restores it as it was, adding it to the container at the
		  depth it was before
		  @param shapeID String with the ID of the shape we are looking for
		*/
		private function restoreRemovedShape(shapeID:String):void{
			var i:uint = 0;

			for(; i<removedShapes.length; ++i){
				if(removedShapes[i].getFullID() == shapeID){
					this.addChildAt(removedShapes[i], removedShapesIndexes[i]);
					removedShapes[i].setSelectionFunction(selectFunction);
					removedShapes.splice(i, 1);
					removedShapesIndexes.splice(i, 1);
					i = removedShapes.length; // To finish the loop
				}
			}
			
		}
		
		/**
		  Redoes the actions invalidations specified by an invalidate-previous-actions
		  action
		*/
		private function redoActionInvalidation(act:Action):void{
			for(var i:uint=1; i<act.getNumberLength(); ++i){
				actions.get(uint(act.getNumber(i))).invalidate();
			}
		}
		
		/**
		  Undoes the actions invalidations specified by an invalidate-previous-actions
		  action, so, basically, it makes them valid them again
		*/
		private function undoActionInvalidation(act:Action):void{
			for(var i:uint=1; i<act.getNumberLength(); ++i){
				actions.get(uint(act.getNumber(i))).makeValid();
			}
			//this.applyAction(actions.get(uint(act.getNumber(act.getNumberLength()-1) )))
		}
		
		/**
		  Undoes the last action if its possible, removes it from the
		  action list and stores it in the redo-action list
		*/
		private function undoOneAction():void{
			var act:Action;
			if(actions.size() > 0){
				//do{
				act = actions.pop();
				//}while(act.isInvalidated() && actions.size() > 0);
				
				//if(! act.isInvalidated()){
				undoAction(act);
				redoActions.push(act);
				//}
			}
		}
		
		/**
		  Redoes the last action if its possible and stores it in
		  the action list again
		*/
		private function redoOneAction():void{
			var act:Action;
			if(redoActions.size() > 0){
				//do{
				act = redoActions.pop();
				//}while(act.isInvalidated() && redoActions.size() > 0);
				
				//if(! act.isInvalidated()){
				applyAction(act);
				actions.push(act);
				//}
			}
		}
		
		/**
		  Gets the array of colors from the action, assuming it is a PAINT action
		  @param act Action to extract the array
		  @return Array of colors, which is an Array of Points of <color:uint, numcoloredlines:uint>
		*/
		private function getPaintColors(act:Action):Array{
			var array:Array = new Array(act.getNumber(0));
			
			for(var i:uint = 0; i<act.getNumber(0); ++i){
				array[i] = new Point(act.getNumber(i*2+1), act.getNumber(i*2+2));
			}
			return array;
		}
		
		/**
		  Gets the array of lines from the action, assuming it is a PAINT action
		  @param act Action to extract the array
		  @return Array of lines, which is like Array<Array<Point> >
		*/
		private function getPaintLines(act:Action):Array{
			var offset:uint = act.getNumber(0)*2+1;
			var array:Array = new Array(act.getNumber(offset));
			var i:uint 		= 0;
			var j:uint		= 0;
			var curr:uint	= offset+1;
			//trace(act.printString());
			
			//trace("curr: "+String(curr));
			//trace("i < num_lineas: "+String(i)+" < "+ String(act.getNumber(offset)));
			for(i = 0; i<act.getNumber(offset); ++i){
				//trace("linea "+String(i)+" tendra "+ String(act.getNumber(curr))+" puntos");
				array[i] = new Array(act.getNumber(curr));
				for(j = 0; j<act.getNumber(curr); ++j){
					//trace("push punto ("+String(act.getNumber(curr+j*2+1))+", "+String(act.getNumber(curr+j*2+2)) +")");
					array[i][j] = new Point(act.getNumber(curr+j*2+1), act.getNumber(curr+j*2+2));
				}
				curr += j*2+1;
				//trace("Posicion actual: "+String(curr));
			}
			//trace(array.toString());
			return array;
		}
		
	}
}